﻿using Masuit.Tools.AspNetCore.Mime;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Masuit.Tools.Files.FileDetector;

public abstract class AbstractCompoundFileDetailDetector : AbstractSignatureDetector
{
	private static readonly SignatureInformation[] CfSignatureInfo = {
		new() { Position = 0, Signature = new byte [] { 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 } },
	};

	/// <inheritdoc />
	protected override SignatureInformation[] SignatureInformations => CfSignatureInfo;

	public abstract IEnumerable<string> Chunks { get; }

	public override string MimeType => new MimeMapper().GetMimeFromExtension("." + Extension);

	public override List<FormatCategory> FormatCategories => GetType().GetCustomAttributes<FormatCategoryAttribute>().Select(a => a.Category).ToList();

	protected abstract bool IsValidChunk(string chunkName, byte[] chunkData);

	public override bool Detect(Stream stream)
	{
		if (!base.Detect(stream))
		{
			return false;
		}

		stream.Position = 0;
		try
		{
			using var cf = new CompoundFile(stream, CFSConfiguration.LeaveOpen | CFSConfiguration.Default);
			foreach (var chunk in Chunks)
			{
				var compoundFileStream = cf.RootStorage.GetStream(chunk);
				if (compoundFileStream == null || !IsValidChunk(chunk, compoundFileStream.GetData()))
				{
					return false;
				}
			}

			return true;
		}
		catch
		{
			return false;
		}
	}

	#region Modified OpenMCDF

	// -------------------------------------------------------------
	// This is a porting from java code, under MIT license of       |
	// the beautiful Red-Black Tree implementation you can find at  |
	// http://en.literateprograms.org/Red-black_tree_(Java)#chunk   |
	// Many Thanks to original Implementors.                        |
	// -------------------------------------------------------------
	internal enum Color
	{ RED = 0, BLACK = 1 }

	internal enum NodeOp
	{
		LAssigned, RAssigned, CAssigned, PAssigned, VAssigned
	}

	internal interface IRBNode : IComparable
	{
		IRBNode Left { get; set; }

		IRBNode Right { get; set; }

		Color Color { get; set; }

		IRBNode Parent { get; set; }

		IRBNode Grandparent();

		IRBNode Sibling();

		IRBNode Uncle();
	}

	internal sealed class RBTree
	{
		public IRBNode Root { get; set; }

		private static Color NodeColor(IRBNode n) => n == null ? Color.BLACK : n.Color;

		public RBTree()
		{ }

		public RBTree(IRBNode root)
		{ Root = root; }

		private IRBNode Lookup(IRBNode template)
		{
			IRBNode n = Root;
			while (n != null)
			{
				int compResult = template.CompareTo(n);
				if (compResult == 0) return n;
				n = compResult < 0 ? n.Left : n.Right;
			}
			return n;
		}

		public bool TryLookup(IRBNode template, out IRBNode val)
		{
			val = Lookup(template);
			return val != null;
		}

		private void Replace(IRBNode oldn, IRBNode newn)
		{
			if (oldn.Parent == null) Root = newn;
			else
			{
				if (oldn == oldn.Parent.Left)
				{
					oldn.Parent.Left = newn;
				}
				else
				{
					oldn.Parent.Right = newn;
				}
			}
			if (newn != null) newn.Parent = oldn.Parent;
		}

		private void RotateL(IRBNode n)
		{
			IRBNode r = n.Right;
			Replace(n, r);
			n.Right = r.Left;
			if (r.Left != null) r.Left.Parent = n;
			r.Left = n;
			n.Parent = r;
		}

		private void RotateR(IRBNode n)
		{
			IRBNode l = n.Left;
			Replace(n, l);
			n.Left = l.Right;
			if (l.Right != null) l.Right.Parent = n;
			l.Right = n;
			n.Parent = l;
		}

		public void Insert(IRBNode newNode)
		{
			newNode.Color = Color.RED;
			IRBNode insertedNode = newNode;

			if (Root == null) Root = insertedNode;
			else
			{
				IRBNode n = Root;
				while (true)
				{
					int compResult = newNode.CompareTo(n);
					if (compResult == 0) throw new Exception($"RBNode {newNode} already present in tree");
					if (compResult < 0)
					{
						if (n.Left == null)
						{
							n.Left = insertedNode;
							break;
						}

						n = n.Left;
					}
					else
					{
						if (n.Right == null)
						{
							n.Right = insertedNode;
							break;
						}
						n = n.Right;
					}
				}
				insertedNode.Parent = n;
			}

			Insert1(insertedNode);
			NodeInserted?.Invoke(insertedNode);
		}

		private void Insert1(IRBNode n)
		{
			if (n.Parent == null) n.Color = Color.BLACK;
			else Insert2(n);
		}

		private void Insert2(IRBNode n)
		{
			if (NodeColor(n.Parent) == Color.BLACK) return;
			Insert3(n);
		}

		private void Insert3(IRBNode n)
		{
			if (NodeColor(n.Uncle()) == Color.RED)
			{
				n.Parent.Color = Color.BLACK;
				n.Uncle().Color = Color.BLACK;
				n.Grandparent().Color = Color.RED;
				Insert1(n.Grandparent());
			}
			else Insert4(n);
		}

		private void Insert4(IRBNode n)
		{
			if (n == n.Parent.Right && n.Parent == n.Grandparent().Left)
			{
				RotateL(n.Parent);
				n = n.Left;
			}
			else if (n == n.Parent.Left && n.Parent == n.Grandparent().Right)
			{
				RotateR(n.Parent);
				n = n.Right;
			}
			Insert5(n);
		}

		private void Insert5(IRBNode n)
		{
			n.Parent.Color = Color.BLACK;
			n.Grandparent().Color = Color.RED;
			if (n == n.Parent.Left && n.Parent == n.Grandparent().Left)
				RotateR(n.Grandparent());
			else RotateL(n.Grandparent());
		}

		private static IRBNode MaximumNode(IRBNode n)
		{
			while (n.Right != null)
				n = n.Right;
			return n;
		}

		public void VisitTree(Action<IRBNode> action)
		{
			IRBNode walker = Root;
			if (walker != null)
				DoVisitTree(action, walker);
		}

		private void DoVisitTree(Action<IRBNode> action, IRBNode walker)
		{
			if (walker.Left != null)
			{
				DoVisitTree(action, walker.Left);
			}

			action?.Invoke(walker);
			if (walker.Right != null)
			{
				DoVisitTree(action, walker.Right);
			}
		}

		internal void VisitTreeNodes(Action<IRBNode> action)
		{
			IRBNode walker = Root;
			if (walker != null)
			{
				DoVisitTreeNodes(action, walker);
			}
		}

		private void DoVisitTreeNodes(Action<IRBNode> action, IRBNode walker)
		{
			if (walker.Left != null)
			{
				DoVisitTreeNodes(action, walker.Left);
			}

			action?.Invoke(walker);
			if (walker.Right != null)
			{
				DoVisitTreeNodes(action, walker.Right);
			}
		}

		public class RBTreeEnumerator : IEnumerator<IRBNode>
		{
			private int position = -1;
			private Queue<IRBNode> heap = new();

			internal RBTreeEnumerator(RBTree tree)
			{ tree.VisitTreeNodes(item => heap.Enqueue(item)); }

			public IRBNode Current => heap.ElementAt(position);

			public void Dispose()
			{ }

			object System.Collections.IEnumerator.Current => heap.ElementAt(position);

			public bool MoveNext() => (++position < heap.Count);

			public void Reset()
			{ position = -1; }
		}

		public RBTreeEnumerator GetEnumerator() => new RBTreeEnumerator(this);

		internal void FireNodeOperation(IRBNode node, NodeOp operation)
		{
			NodeOperation?.Invoke(node, operation);
		}

		internal event Action<IRBNode> NodeInserted;

		internal event Action<IRBNode, NodeOp> NodeOperation;
	}

	/* This Source Code Form is subject to the terms of the Mozilla Public
     * License, v. 2.0. If a copy of the MPL was not distributed with this
     * file, You can obtain one at http://mozilla.org/MPL/2.0/.
     *
     * The Original Code is OpenMCDF - Compound Document Format library.
     *
     * The Initial Developer of the Original Code is Federico Blaseotto.*/

	internal enum SectorType
	{
		Normal, Mini, FAT, DIFAT, RangeLockSector, Directory
	}

	internal sealed class Sector : IDisposable
	{
		public static int MinisectorSize = 64;

		public const int Freesect = unchecked((int)0xFFFFFFFF);
		public const int Endofchain = unchecked((int)0xFFFFFFFE);
		public const int Fatsect = unchecked((int)0xFFFFFFFD);
		public const int Difsect = unchecked((int)0xFFFFFFFC);

		public bool DirtyFlag { get; set; }

		public bool IsStreamed => _stream != null && Size != MinisectorSize && Id * Size + Size < _stream.Length;

		private readonly Stream _stream;

		public Sector(int size, Stream stream)
		{
			this.Size = size;
			this._stream = stream;
		}

		public Sector(int size, byte[] data)
		{
			this.Size = size;
			this._data = data;
			this._stream = null;
		}

		public Sector(int size)
		{
			this.Size = size;
			this._data = null;
			this._stream = null;
		}

		internal SectorType Type { get; set; }

		public int Id { get; set; } = -1;

		public int Size { get; private set; } = 0;

		private byte[] _data;

		public byte[] GetData()
		{
			if (_data == null)
			{
				_data = new byte[Size];

				if (IsStreamed)
				{
					_stream.Seek(Size + Id * Size, SeekOrigin.Begin);
					_stream.Read(_data, 0, Size);
				}
			}

			return _data;
		}

		public void ZeroData()
		{
			_data = new byte[Size];
			DirtyFlag = true;
		}

		public void InitFATData()
		{
			_data = new byte[Size];

			for (int i = 0; i < Size; i++)
				_data[i] = 0xFF;

			DirtyFlag = true;
		}

		internal void ReleaseData() => this._data = null;

		private readonly object _lockObject = new Object();

		#region IDisposable Members

		private bool _disposed;

		void IDisposable.Dispose()
		{
			try
			{
				if (!_disposed)
				{
					lock (_lockObject)
					{
						this._data = null;
						this.DirtyFlag = false;
						this.Id = Sector.Endofchain;
						this.Size = 0;
					}
				}
			}
			finally { _disposed = true; }
			GC.SuppressFinalize(this);
		}

		#endregion IDisposable Members
	}

	internal enum StgType : int
	{
		StgInvalid = 0,
		StgStorage = 1,
		StgStream = 2,
		StgLockbytes = 3,
		StgProperty = 4,
		StgRoot = 5
	}

	internal sealed class DirectoryEntry : IRBNode
	{
		internal const int THIS_IS_GREATER = 1;
		internal const int OTHER_IS_GREATER = -1;
		private IList<DirectoryEntry> dirRepository;

		public int SID { get; set; } = -1;

		internal const Int32 NOSTREAM = unchecked((int)0xFFFFFFFF);

		private DirectoryEntry(String name, StgType stgType, IList<DirectoryEntry> dirRepository)
		{
			this.dirRepository = dirRepository;

			this.StgType = stgType;

			switch (stgType)
			{
				case StgType.StgStream:

					StorageCLSID = new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
					CreationDate = new byte[8];
					ModifyDate = new byte[8];
					break;

				case StgType.StgStorage:
					CreationDate = BitConverter.GetBytes((DateTime.Now.ToFileTime()));
					break;

				case StgType.StgRoot:
					CreationDate = new byte[8];
					ModifyDate = new byte[8];
					break;
			}

			this.SetEntryName(name);
		}

		public byte[] EntryName { get; private set; } = new byte[64];

		public String GetEntryName()
		{
			if (EntryName != null && EntryName.Length > 0)
				return Encoding.Unicode.GetString(EntryName).Remove((NameLength - 1) / 2);
			else return String.Empty;
		}

		public void SetEntryName(String entryName)
		{
			if (entryName.Contains(@"\") || entryName.Contains(@"/") ||
				entryName.Contains(@":") || entryName.Contains(@"!"))
				throw new Exception("Invalid character in entry: the characters '\\', '/', ':','!' cannot be used in entry name");

			if (entryName.Length > 31)
				throw new Exception("Entry name MUST be smaller than 31 characters");

			byte[] newName = null;
			byte[] temp = Encoding.Unicode.GetBytes(entryName);
			newName = new byte[64];
			Buffer.BlockCopy(temp, 0, newName, 0, temp.Length);
			newName[temp.Length] = 0x00;
			newName[temp.Length + 1] = 0x00;

			EntryName = newName;
			NameLength = (ushort)(temp.Length + 2);
		}

		public ushort NameLength { get; private set; }

		public StgType StgType { get; set; } = StgType.StgInvalid;

		public Color Color { get; set; } = Color.BLACK;

		public Int32 LeftSibling { get; set; } = NOSTREAM;

		public Int32 RightSibling { get; set; } = NOSTREAM;

		public Int32 Child { get; set; } = NOSTREAM;

		public Guid StorageCLSID { get; set; } = Guid.NewGuid();

		public Int32 StateBits { get; set; }

		public byte[] CreationDate { get; set; } = new byte[8] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

		public byte[] ModifyDate { get; set; } = new byte[8] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

		public Int32 StartSetc { get; set; } = Sector.Endofchain;

		public long Size { get; set; }

		public int CompareTo(object obj)
		{
			DirectoryEntry otherDir = obj as DirectoryEntry;

			if (otherDir == null)
				throw new Exception("Invalid casting: compared object does not implement IDirectorEntry interface");

			if (this.NameLength > otherDir.NameLength)
				return THIS_IS_GREATER;
			else if (this.NameLength < otherDir.NameLength)
				return OTHER_IS_GREATER;
			else
			{
				String thisName = Encoding.Unicode.GetString(this.EntryName, 0, this.NameLength);
				String otherName = Encoding.Unicode.GetString(otherDir.EntryName, 0, otherDir.NameLength);

				for (int z = 0; z < thisName.Length; z++)
				{
					char thisChar = char.ToUpperInvariant(thisName[z]);
					char otherChar = char.ToUpperInvariant(otherName[z]);

					if (thisChar > otherChar)
						return THIS_IS_GREATER;
					else if (thisChar < otherChar)
						return OTHER_IS_GREATER;
				}

				return 0;
			}
		}

		public override bool Equals(object obj)
		{
			return CompareTo(obj) == 0;
		}

		private static ulong Fnv_hash(byte[] buffer)
		{
			ulong h = 2166136261;
			int i;

			for (i = 0; i < buffer.Length; i++)
				h = (h * 16777619) ^ buffer[i];

			return h;
		}

		public override int GetHashCode()
		{
			return (int)Fnv_hash(EntryName);
		}

		public void Read(Stream stream, int ver = 3)
		{
			using (BinaryReader rw = new BinaryReader(stream, Encoding.UTF8, true))
			{
				EntryName = rw.ReadBytes(64);
				NameLength = rw.ReadUInt16();
				StgType = (StgType)rw.ReadByte();
				Color = (Color)rw.ReadByte();
				LeftSibling = rw.ReadInt32();
				RightSibling = rw.ReadInt32();
				Child = rw.ReadInt32();

				if (StgType == StgType.StgInvalid)
				{
					LeftSibling = NOSTREAM;
					RightSibling = NOSTREAM;
					Child = NOSTREAM;
				}

				StorageCLSID = new Guid(rw.ReadBytes(16));
				StateBits = rw.ReadInt32();
				CreationDate = rw.ReadBytes(8);
				ModifyDate = rw.ReadBytes(8);
				StartSetc = rw.ReadInt32();

				if (ver == 3)
				{
					Size = rw.ReadInt32();
					rw.ReadBytes(4);
				}
				else
					Size = rw.ReadInt64();
			}
		}

		public string Name => GetEntryName();

		public IRBNode Left
		{
			get
			{
				if (LeftSibling == NOSTREAM)
					return null;
				return dirRepository[LeftSibling];
			}

			set
			{
				LeftSibling = value != null ? (value as DirectoryEntry).SID : NOSTREAM;
				if (LeftSibling != NOSTREAM)
					dirRepository[LeftSibling].Parent = this;
			}
		}

		public IRBNode Right
		{
			get
			{
				if (RightSibling == NOSTREAM)
					return null;
				return dirRepository[RightSibling];
			}

			set
			{
				RightSibling = value != null ? ((DirectoryEntry)value).SID : NOSTREAM;
				if (RightSibling != NOSTREAM)
					dirRepository[RightSibling].Parent = this;
			}
		}

		public IRBNode Parent { get; set; }

		public IRBNode Grandparent() => Parent?.Parent;

		public IRBNode Sibling() => (this == Parent.Left) ? Parent.Right : Parent.Left;

		public IRBNode Uncle() => Parent?.Sibling();

		internal static DirectoryEntry New(String name, StgType stgType, IList<DirectoryEntry> dirRepository)
		{
			DirectoryEntry de = null;
			if (dirRepository != null)
			{
				de = new DirectoryEntry(name, stgType, dirRepository);
				dirRepository.Add(de);
				de.SID = dirRepository.Count - 1;
			}
			else
				throw new ArgumentNullException("dirRepository", "Directory repository cannot be null in New() method");

			return de;
		}

		internal static DirectoryEntry Mock(String name, StgType stgType) => new DirectoryEntry(name, stgType, null);

		internal static DirectoryEntry TryNew(String name, StgType stgType, IList<DirectoryEntry> dirRepository)
		{
			DirectoryEntry de = new DirectoryEntry(name, stgType, dirRepository);

			if (de != null)
			{
				for (int i = 0; i < dirRepository.Count; i++)
				{
					if (dirRepository[i].StgType == StgType.StgInvalid)
					{
						dirRepository[i] = de;
						de.SID = i;
						return de;
					}
				}
			}

			dirRepository.Add(de);
			de.SID = dirRepository.Count - 1;

			return de;
		}

		public override string ToString() => $"{Name} [{SID}]{(StgType == StgType.StgStream ? "Stream" : "Storage")}";

		public void AssignValueTo(IRBNode other)
		{
			DirectoryEntry d = other as DirectoryEntry;

			d.SetEntryName(GetEntryName());

			d.CreationDate = new byte[CreationDate.Length];
			CreationDate.CopyTo(d.CreationDate, 0);

			d.ModifyDate = new byte[ModifyDate.Length];
			ModifyDate.CopyTo(d.ModifyDate, 0);

			d.Size = Size;
			d.StartSetc = StartSetc;
			d.StateBits = StateBits;
			d.StgType = StgType;
			d.StorageCLSID = new Guid(StorageCLSID.ToByteArray());
			d.Child = Child;
		}
	}

	internal abstract class CFItem : IComparable<CFItem>
	{
		private CompoundFile compoundFile;

		protected CompoundFile CompoundFile => compoundFile;

		protected void CheckDisposed()
		{
			if (compoundFile.IsClosed)
				throw new ObjectDisposedException("Owner Compound file has been closed and owned items have been invalidated");
		}

		protected CFItem()
		{ }

		protected CFItem(CompoundFile compoundFile)
		{ this.compoundFile = compoundFile; }

		internal DirectoryEntry DirEntry { get; set; }

		internal int CompareTo(CFItem other) => DirEntry.CompareTo(other.DirEntry);

		public int CompareTo(object obj) => DirEntry.CompareTo((obj as CFItem).DirEntry);

		public static bool operator ==(CFItem leftItem, CFItem rightItem)
		{
			if (System.Object.ReferenceEquals(leftItem, rightItem))
				return true;
			if (((object)leftItem == null) || ((object)rightItem == null))
				return false;
			return leftItem.CompareTo(rightItem) == 0;
		}

		public static bool operator !=(CFItem leftItem, CFItem rightItem) => !(leftItem == rightItem);

		public override bool Equals(object obj) => CompareTo(obj) == 0;

		public override int GetHashCode() => DirEntry.GetEntryName().GetHashCode();

		public string Name
		{
			get
			{
				var n = DirEntry.GetEntryName();
				return (n != null && n.Length > 0) ? n.TrimEnd('\0') : string.Empty;
			}
		}

		public long Size => DirEntry.Size;

		public bool IsStorage => DirEntry.StgType == StgType.StgStorage;

		public bool IsStream => DirEntry.StgType == StgType.StgStream;

		public bool IsRoot => DirEntry.StgType == StgType.StgRoot;

		public DateTime CreationDate
		{
			get => DateTime.FromFileTime(BitConverter.ToInt64(DirEntry.CreationDate, 0));

			set
			{
				if (DirEntry.StgType != StgType.StgStream && DirEntry.StgType != StgType.StgRoot)
					DirEntry.CreationDate = BitConverter.GetBytes((value.ToFileTime()));
				else
					throw new Exception("Creation Date can only be set on storage entries");
			}
		}

		public DateTime ModifyDate
		{
			get => DateTime.FromFileTime(BitConverter.ToInt64(DirEntry.ModifyDate, 0));

			set
			{
				if (DirEntry.StgType != StgType.StgStream && DirEntry.StgType != StgType.StgRoot)
					DirEntry.ModifyDate = BitConverter.GetBytes((value.ToFileTime()));
				else
					throw new Exception("Modify Date can only be set on storage entries");
			}
		}

		public Guid CLSID
		{
			get => DirEntry.StorageCLSID;

			set
			{
				if (DirEntry.StgType != StgType.StgStream)
					DirEntry.StorageCLSID = value;
				else
					throw new Exception("Object class GUID can only be set on Root and Storage entries");
			}
		}

		int IComparable<CFItem>.CompareTo(CFItem other) => DirEntry.CompareTo(other.DirEntry);

		public override string ToString()
		{
			return (DirEntry != null)
				? $"[{DirEntry.LeftSibling},{DirEntry.SID},{DirEntry.RightSibling}] {DirEntry.GetEntryName()}"
				: string.Empty;
		}
	}

	internal sealed class CFStream : CFItem
	{
		internal CFStream(CompoundFile compoundFile, DirectoryEntry dirEntry)
			: base(compoundFile)
		{
			if (dirEntry == null || dirEntry.SID < 0)
				throw new Exception("Attempting to add a CFStream using an unitialized directory");
			this.DirEntry = dirEntry;
		}

		public Byte[] GetData()
		{
			CheckDisposed();
			return this.CompoundFile.GetData(this);
		}

		public int Read(byte[] buffer, long position, int count)
		{
			CheckDisposed();
			return this.CompoundFile.ReadData(this, position, buffer, 0, count);
		}

		internal int Read(byte[] buffer, long position, int offset, int count)
		{
			CheckDisposed();
			return this.CompoundFile.ReadData(this, position, buffer, offset, count);
		}
	}

	internal sealed class CFStorage : CFItem
	{
		private RBTree children;

		internal RBTree Children
		{
			get
			{
				if (children == null)
					children = LoadChildren(this.DirEntry.SID) ?? this.CompoundFile.CreateNewTree();
				return children;
			}
		}

		internal CFStorage(CompoundFile compFile, DirectoryEntry dirEntry)
			: base(compFile)
		{
			if (dirEntry == null || dirEntry.SID < 0)
				throw new Exception("Attempting to create a CFStorage using an unitialized directory");
			this.DirEntry = dirEntry;
		}

		private RBTree LoadChildren(int SID)
		{
			RBTree childrenTree = this.CompoundFile.GetChildrenTree(SID);

			if (childrenTree.Root != null)
				this.DirEntry.Child = (childrenTree.Root as DirectoryEntry).SID;
			else
				this.DirEntry.Child = DirectoryEntry.NOSTREAM;

			return childrenTree;
		}

		public CFStream GetStream(String streamName)
		{
			CheckDisposed();

			DirectoryEntry tmp = DirectoryEntry.Mock(streamName, StgType.StgStream);

			if (Children.TryLookup(tmp, out IRBNode outDe) && (((DirectoryEntry)outDe).StgType == StgType.StgStream))
				return new CFStream(this.CompoundFile, (DirectoryEntry)outDe);
			else
				throw new KeyNotFoundException("Cannot find item [" + streamName + "] within the current storage");
		}

		public CFStream TryGetStream(String streamName)
		{
			CheckDisposed();

			DirectoryEntry tmp = DirectoryEntry.Mock(streamName, StgType.StgStream);

			if (Children.TryLookup(tmp, out IRBNode outDe) && ((outDe as DirectoryEntry).StgType == StgType.StgStream))
				return new CFStream(this.CompoundFile, (DirectoryEntry)outDe);
			else
				return null;
		}

		public CFStorage GetStorage(String storageName)
		{
			CheckDisposed();

			DirectoryEntry template = DirectoryEntry.Mock(storageName, StgType.StgInvalid);

			if (Children.TryLookup(template, out IRBNode outDe) && (outDe as DirectoryEntry).StgType == StgType.StgStorage)
				return new CFStorage(this.CompoundFile, outDe as DirectoryEntry);
			else
				throw new KeyNotFoundException("Cannot find item [" + storageName + "] within the current storage");
		}

		public CFStorage TryGetStorage(String storageName)
		{
			CheckDisposed();

			DirectoryEntry template = DirectoryEntry.Mock(storageName, StgType.StgInvalid);

			if (Children.TryLookup(template, out IRBNode outDe) && ((DirectoryEntry)outDe).StgType == StgType.StgStorage)
				return new CFStorage(this.CompoundFile, outDe as DirectoryEntry);
			else
				return null;
		}

		public void VisitEntries(Action<CFItem> action, bool recursive)
		{
			CheckDisposed();

			if (action != null)
			{
				List<IRBNode> subStorages = new List<IRBNode>();

				void internalAction(IRBNode targetNode)
				{
					DirectoryEntry d = targetNode as DirectoryEntry;
					if (d.StgType == StgType.StgStream)
						action(new CFStream(this.CompoundFile, d));
					else
						action(new CFStorage(this.CompoundFile, d));

					if (d.Child != DirectoryEntry.NOSTREAM)
						subStorages.Add(targetNode);

					return;
				}

				this.Children.VisitTreeNodes(internalAction);

				if (recursive && subStorages.Count > 0)
					foreach (IRBNode n in subStorages)
						(new CFStorage(this.CompoundFile, n as DirectoryEntry)).VisitEntries(action, recursive);
			}
		}
	}

	internal class CFItemComparer : IComparer<CFItem>
	{
		public int Compare(CFItem x, CFItem y) => (x.DirEntry.CompareTo(y.DirEntry));
	}

	internal sealed class Header
	{
		public byte[] HeaderSignature { get; private set; } = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 };

		public byte[] CLSID { get; set; } = new byte[16];

		public ushort MinorVersion { get; private set; } = 0x003E;

		public ushort MajorVersion { get; private set; } = 0x0003;

		public ushort ByteOrder { get; private set; } = 0xFFFE;

		public ushort SectorShift { get; private set; } = 9;

		public ushort MiniSectorShift { get; private set; } = 6;

		public int DirectorySectorsNumber { get; set; }

		public int FATSectorsNumber { get; set; }

		public int FirstDirectorySectorID { get; set; } = Sector.Endofchain;

		public uint MinSizeStandardStream { get; set; } = 4096;

		public int FirstMiniFATSectorID { get; set; } = unchecked((int)0xFFFFFFFE);

		public uint MiniFATSectorsNumber { get; set; }

		public int FirstDIFATSectorID { get; set; } = Sector.Endofchain;

		public uint DIFATSectorsNumber { get; set; }

		public int[] DIFAT { get; private set; } = new int[109];

		public Header() : this(3)
		{
		}

		public Header(ushort version)
		{
			switch (version)
			{
				case 3:
					MajorVersion = 3;
					SectorShift = 0x0009;
					break;

				case 4:
					MajorVersion = 4;
					SectorShift = 0x000C;
					break;

				default:
					throw new Exception("Invalid Compound File Format version");
			}

			for (int i = 0; i < 109; i++)
				DIFAT[i] = Sector.Freesect;
		}

		public void Read(Stream stream)
		{
			using (BinaryReader rw = new BinaryReader(stream, Encoding.UTF8, true))
			{
				HeaderSignature = rw.ReadBytes(8);
				CheckSignature();
				CLSID = rw.ReadBytes(16);
				MinorVersion = rw.ReadUInt16();
				MajorVersion = rw.ReadUInt16();
				CheckVersion();
				ByteOrder = rw.ReadUInt16();
				SectorShift = rw.ReadUInt16();
				MiniSectorShift = rw.ReadUInt16();
				rw.ReadBytes(6);
				DirectorySectorsNumber = rw.ReadInt32();
				FATSectorsNumber = rw.ReadInt32();
				FirstDirectorySectorID = rw.ReadInt32();
				rw.ReadUInt32();
				MinSizeStandardStream = rw.ReadUInt32();
				FirstMiniFATSectorID = rw.ReadInt32();
				MiniFATSectorsNumber = rw.ReadUInt32();
				FirstDIFATSectorID = rw.ReadInt32();
				DIFATSectorsNumber = rw.ReadUInt32();

				for (int i = 0; i < 109; i++)
					DIFAT[i] = rw.ReadInt32();
			}
		}

		private void CheckVersion()
		{
			if (MajorVersion != 3 && MajorVersion != 4)
				throw new InvalidDataException("Unsupported Binary File Format version: OpenMcdf only supports Compound Files with major version equal to 3 or 4 ");
		}

		private byte[] OLE_CFS_SIGNATURE = new byte[] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 };

		private void CheckSignature()
		{
			for (int i = 0; i < HeaderSignature.Length; i++)
				if (HeaderSignature[i] != OLE_CFS_SIGNATURE[i])
					throw new InvalidDataException("Invalid OLE structured storage file");
		}
	}

	internal sealed class StreamView : Stream
	{
		private readonly int _sectorSize;
		private long _position;

		private readonly List<Sector> _sectorChain;
		private readonly Stream _stream;
		private readonly bool _isFatStream = false;
		private readonly List<Sector> _freeSectors = new List<Sector>();

		public IEnumerable<Sector> FreeSectors => _freeSectors;

		public StreamView(List<Sector> sectorChain, int sectorSize, Stream stream)
		{
			if (sectorSize <= 0)
				throw new Exception("Sector size must be greater than zero");

			this._sectorChain = sectorChain ?? throw new Exception("Sector Chain cannot be null");
			this._sectorSize = sectorSize;
			this._stream = stream;
		}

		public StreamView(List<Sector> sectorChain, int sectorSize, long length, Queue<Sector> availableSectors, Stream stream, bool isFatStream = false)
			: this(sectorChain, sectorSize, stream)
		{
			this._isFatStream = isFatStream;
			AdjustLength(length, availableSectors);
		}

		public List<Sector> BaseSectorChain => _sectorChain;

		public override bool CanRead => true;

		public override bool CanSeek => true;

		public override bool CanWrite => true;

		public override void Flush()
		{ }

		private long _length;

		public override long Length => _length;

		public override long Position
		{
			get => _position;

			set
			{
				if (_position > _length - 1)
					throw new ArgumentOutOfRangeException("value");
				_position = value;
			}
		}

		private byte[] buf = new byte[4];

		public int ReadInt32()
		{
			this.Read(buf, 0, 4);
			return (((buf[0] | (buf[1] << 8)) | (buf[2] << 16)) | (buf[3] << 24));
		}

		public override int Read(byte[] buffer, int offset, int count)
		{
			int nRead = 0;
			int nToRead = 0;

			if (_sectorChain != null && _sectorChain.Count > 0)
			{
				// First sector
				int secIndex = (int)(_position / _sectorSize);

				nToRead = Math.Min(_sectorChain[0].Size - ((int)_position % _sectorSize), count);

				if (secIndex < _sectorChain.Count)
				{
					Buffer.BlockCopy(_sectorChain[secIndex].GetData(),
						(int)(_position % _sectorSize), buffer, offset, nToRead);
				}

				nRead += nToRead;

				++secIndex;

				// Central sectors
				while (nRead < (count - _sectorSize))
				{
					nToRead = _sectorSize;
					Buffer.BlockCopy(_sectorChain[secIndex].GetData(), 0, buffer, offset + nRead, nToRead);
					nRead += nToRead;
					++secIndex;
				}

				// Last sector
				nToRead = count - nRead;

				if (nToRead != 0)
				{
					Buffer.BlockCopy(_sectorChain[secIndex].GetData(), 0, buffer, offset + nRead, nToRead);
					nRead += nToRead;
				}

				_position += nRead;

				return nRead;
			}
			else
				return 0;
		}

		public override long Seek(long offset, SeekOrigin origin)
		{
			switch (origin)
			{
				case SeekOrigin.Begin: _position = offset; break;
				case SeekOrigin.Current: _position += offset; break;
				case SeekOrigin.End: _position = Length - offset; break;
			}
			AdjustLength(_position);
			return _position;
		}

		private void AdjustLength(long value, Queue<Sector> availableSectors = null)
		{
			this._length = value;
			long delta = value - (_sectorChain.Count * (long)_sectorSize);

			if (delta > 0)
			{
				int nSec = (int)Math.Ceiling(((double)delta / _sectorSize));
				while (nSec > 0)
				{
					Sector t = null;

					if (availableSectors == null || availableSectors.Count == 0)
					{
						t = new Sector(_sectorSize, _stream);
						if (_sectorSize == Sector.MinisectorSize)
							t.Type = SectorType.Mini;
					}
					else
						t = availableSectors.Dequeue();

					if (_isFatStream)
						t.InitFATData();
					_sectorChain.Add(t);
					nSec--;
				}
			}
		}

		public override void SetLength(long value) => AdjustLength(value);

		public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
	}

	[Flags]
	internal enum CFSConfiguration
	{
		Default = 1,
		LeaveOpen = 16,
	}

	internal sealed class CompoundFile : IDisposable
	{
		public CFSConfiguration Configuration { get; private set; } = CFSConfiguration.Default;

		internal int GetSectorSize() => 2 << (header.SectorShift - 1);

		private const int HEADER_DIFAT_ENTRIES_COUNT = 109;
		private readonly int DIFAT_SECTOR_FAT_ENTRIES_COUNT = 127;
		private readonly int FAT_SECTOR_ENTRIES_COUNT = 128;
		private const int SIZE_OF_SID = 4;
		private const int FLUSHING_QUEUE_SIZE = 6000;
		private const int FLUSHING_BUFFER_MAX_SIZE = 1024 * 1024 * 16;

		private List<Sector> sectors = new List<Sector>();

		private Header header;

		internal Stream sourceStream = null;

		public CompoundFile(Stream stream, CFSConfiguration configParameters)
		{
			this.closeStream = !configParameters.HasFlag(CFSConfiguration.LeaveOpen);

			LoadStream(stream);

			DIFAT_SECTOR_FAT_ENTRIES_COUNT = (GetSectorSize() / 4) - 1;
			FAT_SECTOR_ENTRIES_COUNT = (GetSectorSize() / 4);
		}

		private string fileName = string.Empty;

		private void Load(Stream stream)
		{
			try
			{
				this.header = new Header();
				this.directoryEntries = new List<DirectoryEntry>();

				this.sourceStream = stream;

				header.Read(stream);

				int n_sector = Ceiling(((stream.Length - GetSectorSize()) / (double)GetSectorSize()));

				if (stream.Length > 0x7FFFFF0)
					this._transactionLockAllocated = true;

				sectors = new List<Sector>();
				for (int i = 0; i < n_sector; i++)
					sectors.Add(null);

				LoadDirectories();

				this.RootStorage = new CFStorage(this, directoryEntries[0]);
			}
			catch (Exception)
			{
				if (stream != null && closeStream)
					stream.Dispose();
				throw;
			}
		}

		private void LoadFile(String fileName)
		{
			this.fileName = fileName;
			FileStream fs = null;

			try
			{
				fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
				Load(fs);
			}
			catch
			{
				if (fs != null)
					fs.Dispose();
				throw;
			}
		}

		private void LoadStream(Stream stream)
		{
			if (stream == null)
				throw new Exception("Stream parameter cannot be null");
			if (!stream.CanSeek)
				throw new Exception("Cannot load a non-seekable Stream");
			stream.Seek(0, SeekOrigin.Begin);
			Load(stream);
		}

		public bool HasSourceStream => sourceStream != null;

		private void PersistMiniStreamToStream(List<Sector> miniSectorChain)
		{
			List<Sector> miniStream = GetSectorChain(RootEntry.StartSetc, SectorType.Normal);

			StreamView miniStreamView = new StreamView(miniStream, GetSectorSize(), this.RootStorage.Size, null, sourceStream);

			for (int i = 0; i < miniSectorChain.Count; i++)
			{
				Sector s = miniSectorChain[i];

				if (s.Id == -1)
					throw new Exception("Invalid minisector index");

				miniStreamView.Seek(Sector.MinisectorSize * s.Id, SeekOrigin.Begin);
				miniStreamView.Write(s.GetData(), 0, Sector.MinisectorSize);
			}
		}

		private void AllocateMiniSectorChain(List<Sector> sectorChain)
		{
			List<Sector> miniFAT = GetSectorChain(header.FirstMiniFATSectorID, SectorType.Normal);
			List<Sector> miniStream = GetSectorChain(RootEntry.StartSetc, SectorType.Normal);

			StreamView miniFATView = new StreamView(miniFAT, GetSectorSize(),
				header.MiniFATSectorsNumber * Sector.MinisectorSize,
				null, this.sourceStream, true);

			StreamView miniStreamView = new StreamView(miniStream, GetSectorSize(),
				this.RootStorage.Size, null, sourceStream);

			for (int i = 0; i < sectorChain.Count; i++)
			{
				Sector s = sectorChain[i];

				if (s.Id == -1)
				{
					miniStreamView.Seek(this.RootStorage.Size + Sector.MinisectorSize, SeekOrigin.Begin);
					s.Id = (int)(miniStreamView.Position - Sector.MinisectorSize) / Sector.MinisectorSize;

					this.RootStorage.DirEntry.Size = miniStreamView.Length;
				}
			}

			for (int i = 0; i < sectorChain.Count - 1; i++)
			{
				Int32 currentId = sectorChain[i].Id;
				Int32 nextId = sectorChain[i + 1].Id;

				miniFATView.Seek(currentId * 4, SeekOrigin.Begin);
				miniFATView.Write(BitConverter.GetBytes(nextId), 0, 4);
			}

			miniFATView.Seek(sectorChain[sectorChain.Count - 1].Id * SIZE_OF_SID, SeekOrigin.Begin);
			miniFATView.Write(BitConverter.GetBytes(Sector.Endofchain), 0, 4);

			AllocateSectorChain(miniStreamView.BaseSectorChain);
			AllocateSectorChain(miniFATView.BaseSectorChain);

			if (miniFAT.Count > 0)
			{
				this.RootStorage.DirEntry.StartSetc = miniStream[0].Id;
				header.MiniFATSectorsNumber = (uint)miniFAT.Count;
				header.FirstMiniFATSectorID = miniFAT[0].Id;
			}
		}

		private void SetSectorChain(List<Sector> sectorChain)
		{
			if (sectorChain == null || sectorChain.Count == 0)
				return;

			SectorType _st = sectorChain[0].Type;

			if (_st == SectorType.Normal)
				AllocateSectorChain(sectorChain);
			else if (_st == SectorType.Mini)
				AllocateMiniSectorChain(sectorChain);
		}

		private void AllocateSectorChain(List<Sector> sectorChain)
		{
			foreach (Sector s in sectorChain)
			{
				if (s.Id == -1)
				{
					sectors.Add(s);
					s.Id = sectors.Count - 1;
				}
			}

			AllocateFATSectorChain(sectorChain);
		}

		internal bool _transactionLockAdded = false;
		internal int _lockSectorId = -1;
		internal bool _transactionLockAllocated = false;

		private void CheckForLockSector()
		{
			if (_transactionLockAdded && !_transactionLockAllocated)
			{
				StreamView fatStream = new StreamView(GetFatSectorChain(), GetSectorSize(), sourceStream);

				fatStream.Seek(_lockSectorId * 4, SeekOrigin.Begin);
				fatStream.Write(BitConverter.GetBytes(Sector.Endofchain), 0, 4);

				_transactionLockAllocated = true;
			}
		}

		private void AllocateFATSectorChain(List<Sector> sectorChain)
		{
			List<Sector> fatSectors = GetSectorChain(-1, SectorType.FAT);

			StreamView fatStream = new StreamView(fatSectors, GetSectorSize(),
				header.FATSectorsNumber * GetSectorSize(), null,
				sourceStream, true);

			for (int i = 0; i < sectorChain.Count - 1; i++)
			{
				Sector sN = sectorChain[i + 1];
				Sector sC = sectorChain[i];

				fatStream.Seek(sC.Id * 4, SeekOrigin.Begin);
				fatStream.Write(BitConverter.GetBytes(sN.Id), 0, 4);
			}

			fatStream.Seek(sectorChain[sectorChain.Count - 1].Id * 4, SeekOrigin.Begin);
			fatStream.Write(BitConverter.GetBytes(Sector.Endofchain), 0, 4);

			AllocateDIFATSectorChain(fatStream.BaseSectorChain);
		}

		private void AllocateDIFATSectorChain(List<Sector> FATsectorChain)
		{
			header.FATSectorsNumber = FATsectorChain.Count;

			foreach (Sector s in FATsectorChain)
			{
				if (s.Id == -1)
				{
					sectors.Add(s);
					s.Id = sectors.Count - 1;
					s.Type = SectorType.FAT;
				}
			}

			int nCurrentSectors = sectors.Count;

			int nDIFATSectors = (int)header.DIFATSectorsNumber;

			if (FATsectorChain.Count > HEADER_DIFAT_ENTRIES_COUNT)
			{
				nDIFATSectors = Ceiling((double)(FATsectorChain.Count - HEADER_DIFAT_ENTRIES_COUNT) / DIFAT_SECTOR_FAT_ENTRIES_COUNT);
				nDIFATSectors = LowSaturation(nDIFATSectors - (int)header.DIFATSectorsNumber); //required DIFAT
			}

			nCurrentSectors += nDIFATSectors;

			while (header.FATSectorsNumber * FAT_SECTOR_ENTRIES_COUNT < nCurrentSectors)
			{
				Sector extraFATSector = new Sector(GetSectorSize(), sourceStream);
				sectors.Add(extraFATSector);

				extraFATSector.Id = sectors.Count - 1;
				extraFATSector.Type = SectorType.FAT;

				FATsectorChain.Add(extraFATSector);

				header.FATSectorsNumber++;
				nCurrentSectors++;

				if (nDIFATSectors * DIFAT_SECTOR_FAT_ENTRIES_COUNT <
					(header.FATSectorsNumber > HEADER_DIFAT_ENTRIES_COUNT ?
						header.FATSectorsNumber - HEADER_DIFAT_ENTRIES_COUNT :
						0))
				{
					nDIFATSectors++;
					nCurrentSectors++;
				}
			}

			List<Sector> difatSectors = GetSectorChain(-1, SectorType.DIFAT);
			StreamView difatStream = new StreamView(difatSectors, GetSectorSize(), sourceStream);

			for (int i = 0; i < FATsectorChain.Count; i++)
			{
				if (i < HEADER_DIFAT_ENTRIES_COUNT)
					header.DIFAT[i] = FATsectorChain[i].Id;
				else
				{
					if (i != HEADER_DIFAT_ENTRIES_COUNT && (i - HEADER_DIFAT_ENTRIES_COUNT) % DIFAT_SECTOR_FAT_ENTRIES_COUNT == 0)
					{
						difatStream.Write(new byte[sizeof(int)], 0, sizeof(int));
					}

					difatStream.Write(BitConverter.GetBytes(FATsectorChain[i].Id), 0, sizeof(int));
				}
			}

			for (int i = 0; i < difatStream.BaseSectorChain.Count; i++)
			{
				if (difatStream.BaseSectorChain[i].Id == -1)
				{
					sectors.Add(difatStream.BaseSectorChain[i]);
					difatStream.BaseSectorChain[i].Id = sectors.Count - 1;
					difatStream.BaseSectorChain[i].Type = SectorType.DIFAT;
				}
			}

			header.DIFATSectorsNumber = (uint)nDIFATSectors;

			if (difatStream.BaseSectorChain != null && difatStream.BaseSectorChain.Count > 0)
			{
				header.FirstDIFATSectorID = difatStream.BaseSectorChain[0].Id;
				header.DIFATSectorsNumber = (uint)difatStream.BaseSectorChain.Count;

				for (int i = 0; i < difatStream.BaseSectorChain.Count - 1; i++)
					Buffer.BlockCopy(BitConverter.GetBytes(difatStream.BaseSectorChain[i + 1].Id),
						0, difatStream.BaseSectorChain[i].GetData(),
						GetSectorSize() - sizeof(int), 4);

				Buffer.BlockCopy(BitConverter.GetBytes(Sector.Endofchain), 0,
					difatStream.BaseSectorChain[difatStream.BaseSectorChain.Count - 1].GetData(),
					GetSectorSize() - sizeof(int), sizeof(int));
			}
			else header.FirstDIFATSectorID = Sector.Endofchain;

			StreamView fatSv = new StreamView(FATsectorChain, GetSectorSize(), header.FATSectorsNumber * GetSectorSize(), null, sourceStream);

			for (int i = 0; i < header.DIFATSectorsNumber; i++)
			{
				fatSv.Seek(difatStream.BaseSectorChain[i].Id * 4, SeekOrigin.Begin);
				fatSv.Write(BitConverter.GetBytes(Sector.Difsect), 0, 4);
			}

			for (int i = 0; i < header.FATSectorsNumber; i++)
			{
				fatSv.Seek(fatSv.BaseSectorChain[i].Id * 4, SeekOrigin.Begin);
				fatSv.Write(BitConverter.GetBytes(Sector.Fatsect), 0, 4);
			}

			header.FATSectorsNumber = fatSv.BaseSectorChain.Count;
		}

		private List<Sector> GetDifatSectorChain()
		{
			int validationCount = 0;

			List<Sector> result = new List<Sector>();

			int nextSecID
				= Sector.Endofchain;

			if (header.DIFATSectorsNumber != 0)
			{
				validationCount = (int)header.DIFATSectorsNumber;

				Sector s = sectors[header.FirstDIFATSectorID];

				if (s == null)
				{
					sectors[header.FirstDIFATSectorID] = s = new Sector(GetSectorSize(), sourceStream)
					{
						Type = SectorType.DIFAT,
						Id = header.FirstDIFATSectorID
					};
				}

				result.Add(s);

				while (true && validationCount >= 0)
				{
					nextSecID = BitConverter.ToInt32(s.GetData(), GetSectorSize() - 4);

					if (nextSecID == Sector.Freesect || nextSecID == Sector.Endofchain) break;

					validationCount--;

					if (validationCount < 0)
					{
						Dispose();
						throw new InvalidDataException("DIFAT sectors count mismatched. Corrupted compound file");
					}

					s = sectors[nextSecID];

					if (s == null)
						sectors[nextSecID] = s = new Sector(GetSectorSize(), sourceStream) { Id = nextSecID };

					result.Add(s);
				}
			}

			return result;
		}

		private List<Sector> GetFatSectorChain()
		{
			int N_HEADER_FAT_ENTRY = 109;

			List<Sector> result = new List<Sector>();
			int nextSecID = Sector.Endofchain;
			List<Sector> difatSectors = GetDifatSectorChain();

			int idx = 0;
			while (idx < header.FATSectorsNumber && idx < N_HEADER_FAT_ENTRY)
			{
				nextSecID = header.DIFAT[idx];
				Sector s = sectors[nextSecID];

				if (s == null)
				{
					sectors[nextSecID] = s = new Sector(GetSectorSize(), sourceStream)
					{
						Id = nextSecID,
						Type = SectorType.FAT
					};
				}

				result.Add(s);
				++idx;
			}

			if (difatSectors.Count > 0)
			{
				var difatStream = new StreamView(difatSectors, GetSectorSize(),
					header.FATSectorsNumber > N_HEADER_FAT_ENTRY ? (header.FATSectorsNumber - N_HEADER_FAT_ENTRY) * 4 : 0,
					null, sourceStream);

				byte[] nextDIFATSectorBuffer = new byte[4];
				difatStream.Read(nextDIFATSectorBuffer, 0, 4);
				nextSecID = BitConverter.ToInt32(nextDIFATSectorBuffer, 0);

				int i = 0;
				int nFat = N_HEADER_FAT_ENTRY;

				while (nFat < header.FATSectorsNumber)
				{
					if (difatStream.Position == ((GetSectorSize() - 4) + i * GetSectorSize()))
					{
						difatStream.Seek(4, SeekOrigin.Current);
						++i;
						continue;
					}

					Sector s = sectors[nextSecID];

					if (s == null)
					{
						sectors[nextSecID] = s = new Sector(GetSectorSize(), sourceStream)
						{
							Type = SectorType.FAT,
							Id = nextSecID
						};
					}

					result.Add(s);

					difatStream.Read(nextDIFATSectorBuffer, 0, 4);
					nextSecID = BitConverter.ToInt32(nextDIFATSectorBuffer, 0);
					nFat++;
				}
			}
			return result;
		}

		private List<Sector> GetNormalSectorChain(int secID)
		{
			List<Sector> result = new List<Sector>();

			int nextSecID = secID;

			List<Sector> fatSectors = GetFatSectorChain();

			var fatStream = new StreamView(fatSectors, GetSectorSize(), fatSectors.Count * GetSectorSize(), null, sourceStream);

			while (true)
			{
				if (nextSecID == Sector.Endofchain) break;

				if (nextSecID < 0)
					throw new InvalidDataException(String.Format("Next Sector ID reference is below zero. NextID : {0}", nextSecID));

				if (nextSecID >= sectors.Count)
					throw new InvalidDataException(String.Format("Next Sector ID reference an out of range sector. NextID : {0} while sector count {1}", nextSecID, sectors.Count));

				Sector s = sectors[nextSecID];
				if (s == null)
				{
					sectors[nextSecID] = s = new Sector(GetSectorSize(), sourceStream)
					{
						Id = nextSecID,
						Type = SectorType.Normal
					};
				}

				result.Add(s);

				fatStream.Seek(nextSecID * 4, SeekOrigin.Begin);
				int next = fatStream.ReadInt32();

				if (next != nextSecID)
					nextSecID = next;
				else
					throw new InvalidDataException("Cyclic sector chain found. File is corrupted");
			}

			return result;
		}

		private List<Sector> GetMiniSectorChain(int secID)
		{
			List<Sector> result = new List<Sector>();

			if (secID != Sector.Endofchain)
			{
				int nextSecID = secID;

				List<Sector> miniFAT = GetNormalSectorChain(header.FirstMiniFATSectorID);
				List<Sector> miniStream = GetNormalSectorChain(RootEntry.StartSetc);

				StreamView miniFATView = new StreamView(miniFAT, GetSectorSize(), header.MiniFATSectorsNumber * Sector.MinisectorSize, null, sourceStream);
				StreamView miniStreamView = new StreamView(miniStream, GetSectorSize(), RootStorage.Size, null, sourceStream);
				BinaryReader miniFATReader = new BinaryReader(miniFATView);

				nextSecID = secID;

				while (true)
				{
					if (nextSecID == Sector.Endofchain)
						break;

					Sector ms = new Sector(Sector.MinisectorSize, sourceStream);
					byte[] temp = new byte[Sector.MinisectorSize];

					ms.Id = nextSecID;
					ms.Type = SectorType.Mini;

					miniStreamView.Seek(nextSecID * Sector.MinisectorSize, SeekOrigin.Begin);
					miniStreamView.Read(ms.GetData(), 0, Sector.MinisectorSize);

					result.Add(ms);

					miniFATView.Seek(nextSecID * 4, SeekOrigin.Begin);
					nextSecID = miniFATReader.ReadInt32();
				}
			}
			return result;
		}

		internal List<Sector> GetSectorChain(int secID, SectorType chainType)
		{
			switch (chainType)
			{
				case SectorType.DIFAT:
					return GetDifatSectorChain();

				case SectorType.FAT:
					return GetFatSectorChain();

				case SectorType.Normal:
					return GetNormalSectorChain(secID);

				case SectorType.Mini:
					return GetMiniSectorChain(secID);

				default:
					throw new Exception("Unsupproted chain type");
			}
		}

		public CFStorage RootStorage { get; private set; }

		public int Version => this.header.MajorVersion;

		internal RBTree CreateNewTree()
		{
			RBTree bst = new RBTree();
			return bst;
		}

		internal RBTree GetChildrenTree(int sid)
		{
			RBTree bst = new RBTree();
			DoLoadChildren(bst, directoryEntries[sid]);
			return bst;
		}

		private RBTree DoLoadChildrenTrusted(DirectoryEntry de)
		{
			RBTree bst = null;
			if (de.Child != DirectoryEntry.NOSTREAM)
				bst = new RBTree(directoryEntries[de.Child]);
			return bst;
		}

		private void DoLoadChildren(RBTree bst, DirectoryEntry de)
		{
			if (de.Child != DirectoryEntry.NOSTREAM)
			{
				if (directoryEntries[de.Child].StgType == StgType.StgInvalid) return;

				LoadSiblings(bst, directoryEntries[de.Child]);
				NullifyChildNodes(directoryEntries[de.Child]);
				bst.Insert(directoryEntries[de.Child]);
			}
		}

		private void NullifyChildNodes(DirectoryEntry de)
		{
			de.Parent = null;
			de.Left = null;
			de.Right = null;
		}

		private readonly List<int> _levelSiDs = new List<int>();

		private void LoadSiblings(RBTree bst, DirectoryEntry de)
		{
			_levelSiDs.Clear();

			if (de.LeftSibling != DirectoryEntry.NOSTREAM)
				DoLoadSiblings(bst, directoryEntries[de.LeftSibling]);

			if (de.RightSibling != DirectoryEntry.NOSTREAM)
			{
				_levelSiDs.Add(de.RightSibling);
				DoLoadSiblings(bst, directoryEntries[de.RightSibling]);
			}
		}

		private void DoLoadSiblings(RBTree bst, DirectoryEntry de)
		{
			if (ValidateSibling(de.LeftSibling))
			{
				_levelSiDs.Add(de.LeftSibling);
				DoLoadSiblings(bst, directoryEntries[de.LeftSibling]);
			}

			if (ValidateSibling(de.RightSibling))
			{
				_levelSiDs.Add(de.RightSibling);
				DoLoadSiblings(bst, directoryEntries[de.RightSibling]);
			}

			NullifyChildNodes(de);
			bst.Insert(de);
		}

		private bool ValidateSibling(int sid)
		{
			if (sid != DirectoryEntry.NOSTREAM)
			{
				if (sid >= directoryEntries.Count)
					return false;

				if (directoryEntries[sid].StgType == StgType.StgInvalid)
					return false;

				if (!Enum.IsDefined(typeof(StgType), directoryEntries[sid].StgType))
					return false;

				if (_levelSiDs.Contains(sid))
					throw new InvalidDataException("Cyclic reference of directory item");

				return true;
			}

			return false;
		}

		private void LoadDirectories()
		{
			List<Sector> directoryChain = GetSectorChain(header.FirstDirectorySectorID, SectorType.Normal);

			if (header.FirstDirectorySectorID == Sector.Endofchain)
				header.FirstDirectorySectorID = directoryChain[0].Id;

			StreamView dirReader = new StreamView(directoryChain, GetSectorSize(), directoryChain.Count * GetSectorSize(), null, sourceStream);

			while (dirReader.Position < directoryChain.Count * GetSectorSize())
			{
				DirectoryEntry de = DirectoryEntry.New(String.Empty, StgType.StgInvalid, directoryEntries);
				de.Read(dirReader, this.Version);
			}
		}

		private void CheckFileLength() => throw new NotImplementedException();

		internal int ReadData(CFStream cFStream, long position, byte[] buffer, int count)
		{
			if (count > buffer.Length)
				throw new ArgumentException("count parameter exceeds buffer size");

			DirectoryEntry de = cFStream.DirEntry;

			count = (int)Math.Min(de.Size - position, count);

			StreamView sView = null;
			if (de.Size < header.MinSizeStandardStream)
				sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Mini), Sector.MinisectorSize, de.Size, null, sourceStream);
			else
				sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Normal), GetSectorSize(), de.Size, null, sourceStream);

			sView.Seek(position, SeekOrigin.Begin);
			int result = sView.Read(buffer, 0, count);

			return result;
		}

		internal int ReadData(CFStream cFStream, long position, byte[] buffer, int offset, int count)
		{
			DirectoryEntry de = cFStream.DirEntry;

			count = (int)Math.Min(de.Size - offset, count);

			StreamView sView = null;
			if (de.Size < header.MinSizeStandardStream)
				sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Mini), Sector.MinisectorSize, de.Size, null, sourceStream);
			else
				sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Normal), GetSectorSize(), de.Size, null, sourceStream);

			sView.Seek(position, SeekOrigin.Begin);
			int result = sView.Read(buffer, offset, count);

			return result;
		}

		internal byte[] GetData(CFStream cFStream)
		{
			AssertDisposed();

			byte[] result = null;

			DirectoryEntry de = cFStream.DirEntry;

			if (de.Size < header.MinSizeStandardStream)
			{
				var miniView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Mini), Sector.MinisectorSize, de.Size, null, sourceStream);

				using (BinaryReader br = new BinaryReader(miniView))
					result = br.ReadBytes((int)de.Size);
			}
			else
			{
				var sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Normal), GetSectorSize(), de.Size, null, sourceStream);
				result = new byte[(int)de.Size];
				sView.Read(result, 0, result.Length);
			}

			return result;
		}

		public byte[] GetDataBySID(int sid)
		{
			AssertDisposed();
			if (sid < 0)
				return null;
			byte[] result = null;
			try
			{
				DirectoryEntry de = directoryEntries[sid];
				if (de.Size < header.MinSizeStandardStream)
				{
					var miniView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Mini), Sector.MinisectorSize, de.Size, null, sourceStream);
					BinaryReader br = new BinaryReader(miniView);
					result = br.ReadBytes((int)de.Size);
					br.Dispose();
				}
				else
				{
					var sView = new StreamView(GetSectorChain(de.StartSetc, SectorType.Normal), GetSectorSize(), de.Size, null, sourceStream);
					result = new byte[(int)de.Size];
					sView.Read(result, 0, result.Length);
				}
			}
			catch
			{
				throw new Exception("Cannot get data for SID");
			}
			return result;
		}

		public Guid GetGuidBySID(int sid)
		{
			AssertDisposed();
			if (sid < 0)
				throw new Exception("Invalid SID");
			DirectoryEntry de = directoryEntries[sid];
			return de.StorageCLSID;
		}

		public Guid GetGuidForStream(int sid)
		{
			AssertDisposed();
			if (sid < 0)
				throw new Exception("Invalid SID");
			Guid g = new Guid("00000000000000000000000000000000");
			for (int i = sid - 1; i >= 0; i--)
			{
				if (directoryEntries[i].StorageCLSID != g && directoryEntries[i].StgType == StgType.StgStorage)
					return directoryEntries[i].StorageCLSID;
			}
			return g;
		}

		private static int Ceiling(double d) => (int)Math.Ceiling(d);

		private static int LowSaturation(int i) => i > 0 ? i : 0;

		private bool closeStream = true;

		internal bool IsClosed => _disposed;

		#region IDisposable Members

		private bool _disposed;//false
		private object lockObject = new Object();

		public void Dispose()
		{
			try
			{
				if (!_disposed)
				{
					lock (lockObject)
					{
						if (sectors != null)
						{
							sectors.Clear();
							sectors = null;
						}

						this.RootStorage = null;
						this.header = null;
						this.directoryEntries.Clear();
						this.directoryEntries = null;
						this.fileName = null;
					}

					if (this.sourceStream != null && closeStream && !Configuration.HasFlag(CFSConfiguration.LeaveOpen))
						this.sourceStream.Dispose();
				}
			}
			finally
			{
				_disposed = true;
			}
			GC.SuppressFinalize(this);
		}

		#endregion IDisposable Members

		private List<DirectoryEntry> directoryEntries = new List<DirectoryEntry>();

		internal IList<DirectoryEntry> GetDirectories() => directoryEntries;

		internal DirectoryEntry RootEntry => directoryEntries[0];

		private IList<DirectoryEntry> FindDirectoryEntries(String entryName)
		{
			List<DirectoryEntry> result = new List<DirectoryEntry>();

			foreach (DirectoryEntry d in directoryEntries)
				if (d.GetEntryName() == entryName && d.StgType != StgType.StgInvalid)
					result.Add(d);

			return result;
		}

		public IList<CFItem> GetAllNamedEntries(String entryName)
		{
			IList<DirectoryEntry> r = FindDirectoryEntries(entryName);
			List<CFItem> result = new List<CFItem>();

			foreach (DirectoryEntry id in r)
				if (id.GetEntryName() == entryName && id.StgType != StgType.StgInvalid)
					result.Add(id.StgType == StgType.StgStorage ? new CFStorage(this, id) : new CFStream(this, id));

			return result;
		}

		public int GetNumDirectories()
		{
			AssertDisposed();
			return directoryEntries.Count;
		}

		public string GetNameDirEntry(int id)
		{
			AssertDisposed();
			if (id < 0)
				throw new Exception("Invalid Storage ID");
			return directoryEntries[id].Name;
		}

		public StgType GetStorageType(int id)
		{
			AssertDisposed();
			if (id < 0)
				throw new Exception("Invalid Storage ID");
			return directoryEntries[id].StgType;
		}

		private void AssertDisposed()
		{
			if (_disposed)
				throw new ObjectDisposedException("Compound File closed: cannot access data");
		}
	}

	#endregion Modified OpenMCDF
}
